/*    This file is part of unfy.
 *
 *    Unfy is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Lesser General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    Unfy is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General Public License
 *    along with unfy.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <ctype.h>
#include <endian.h>
#include <rid.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define STYLE_9
#include <fn85.h>
#include <rid_fn85.h>

#include "unfy.h"
#include "unfp.h"

#define pass(str) (sizeof(str) - 1)

/* term */

void
unfp_term_print(const Unfy_term *term)
{
	Rid_fn85 str;

	if (!term) {
		printf("EMPTY");
		return;
	}

	switch (term->type) {
	case UNFY_CONST:
		printf("const ");
		break;
	case UNFY_ORDER:
		printf("order %zu", term->u.order);
		return;
	case UNFY_IGN:
		printf("_");
		return;
	case UNFY_LIST:
		unfp_list_print(term->u.list);
		return;
	case UNFY_VAR:
		printf("var ");
		break;
	}

	rid_fn85(str, term->u.id);
	printf("%s", str);
}

Unfy_term *
unfp_term_parse(const char *str, size_t *pos,
		const char **error, Unfy_recycle *rec)
{
	Unfy_type type;
	Rid id;
	Unfy_term *term;
	Unfy_list *list;
	size_t order;
	size_t tmp;
	int scanf_pos = 0;

	switch (*str) {
	case 'c':
		if (strncmp(str, "const", pass("const"))) {
			*error = "Invalid type";
			return NULL;
		}

		*pos += pass("const");
		str += pass("const");
		type = UNFY_CONST;
		break;
	case 'o':
		if (strncmp(str, "order", pass("order"))) {
			*error = "Invalid type";
			return NULL;
		}

		*pos += pass("order");
		str += pass("order");
		tmp = *pos;

		if (unfp_space(str, pos, error) < 0)
			return NULL;

		str += *pos - tmp;

		if (1 != sscanf(str, "%zu%n", &order, &scanf_pos)) {
			*pos += scanf_pos;
			*error = "invalid digit for order";
			return NULL;
		}

		*pos += scanf_pos;

		if (!(term = unfy_term(UNFY_ORDER, &order, rec)))
			*error = "Failed to allocate term";

	        return term;
	case '_':
	        *pos += 1;

		if (!(term = unfy_term(UNFY_IGN, NULL, rec)))
			*error = "Failed to allocate term";

		return term;

	case '(':
	        if (unfp_list_parse(&list, str, pos, error, rec) < 0)
			return NULL;

		if (!(term = unfy_term(UNFY_LIST, list, rec))) {
			*error = "Failed to allocate term";
			unfy_recycle_list(rec, list);
			return NULL;
		}

		return term;
	case 'v':
		if (strncmp(str, "var", pass("var"))) {
			*error = "Invalid type";
			return NULL;
		}

		*pos += pass("var");
		str += pass("var");
		type = UNFY_VAR;
		break;
	default:
		*error = "Invalid type";
		return NULL;
	}

	tmp = *pos;

	if (unfp_space(str, pos, error) < 0)
		return NULL;

	str += *pos - tmp;

	if (rid_fn85_parse(id, str, pos, error) != FN85_OKAY)
		return NULL;

	term = unfy_term(type, id, rec);

	if (!term)
		*error = "Failed to allocate term";

	return term;
}

size_t
unfp_term_len(const Unfy_term *term)
{
	if (!term)
		return 1;

	switch (term->type) {
	case UNFY_CONST:
	case UNFY_VAR:
		return sizeof(Rid) + 1;
	case UNFY_ORDER:
		return sizeof(uint64_t) + 1;
	case UNFY_IGN:
		return 1;
	case UNFY_LIST:
		return unfp_list_len(term->u.list);
	}

	return 0;
}

size_t
unfp_term_write(const Unfy_term *term, unsigned char *buf)
{
	if (!term) {
		*buf = UNFP_TNIL;
		return 1;
	}

	switch (term->type) {
	case UNFY_CONST:
		*buf = UNFP_CONST;
		rid_set(buf + 1, term->u.id);
	        return sizeof(Rid) + 1;
	case UNFY_ORDER:
		*buf = UNFP_ORDER;
		*(uint64_t *) (buf + 1) = htole64(term->u.order);
		return sizeof(uint64_t) + 1;
	case UNFY_IGN:
		*buf = UNFP_IGN;
		return 1;
	case UNFY_VAR:
		*buf = UNFP_VAR;
		rid_set(buf + 1, term->u.id);
		return sizeof(Rid) + 1;
	case UNFY_LIST:
		return unfp_list_write(term->u.list, buf);
	}

	return 0;
}

int
unfp_term_ser(const Unfy_term *term, unsigned char **buf,
	      size_t *size, size_t *pos)
{
	size_t len = unfp_term_len(term);

	if (*size < len) {
		unsigned char *new_buf = realloc(*buf, len);

		if (!new_buf)
			return -1;

	        *buf = new_buf;
		*size = len;
	}

	unfp_term_write(term, *buf);
	*pos += len;
	return 0;
}

int
unfp_term_deser(Unfy_term **term, const unsigned char *buf,
		size_t *pos, Unfy_recycle *rec)
{
	Unfy_list *list;
	size_t order;

	switch (*buf) {
	case UNFP_TNIL:
		++*pos;
		*term = NULL;
		return 0;
	case UNFP_CONST:
		*pos += sizeof(Rid) + 1;
		*term = unfy_term_id(UNFY_CONST, buf + 1, rec);
		return *term ? 0 : -1;
	case UNFP_ORDER:
		*pos += sizeof(uint64_t) + 1;
	        order = le64toh(*(uint64_t *) (buf + 1));
		*term = unfy_term(UNFY_ORDER, &order, rec);
		return *term ? 0 : -1;
	case UNFP_IGN:
		++*pos;
		*term =  unfy_term(UNFY_IGN, NULL, rec);
		return *term ? 0 : -1;
	case UNFP_VAR:
		*pos += sizeof(Rid) + 1;
		*term = unfy_term_id(UNFY_VAR, buf + 1, rec);
		return *term ? 0 : -1;
	case UNFP_LOPEN:
	case UNFP_LNIL:
		break;
	default:
		return -1;
	}

	if (unfp_list_deser(&list, buf, pos, rec) < 0)
		return -1;

	if (!(*term = unfy_term(UNFY_LIST, list, rec))) {
		unfy_recycle_list(rec, list);
		return -1;
	}

	return 0;
}

const char *
unfp_term_deserable(const unsigned char *buf, size_t *pos, const size_t max)
{
	if (!buf || *pos >= max)
		return "Not enough space for term";

	switch (*buf) {
	case UNFP_TNIL:
	case UNFP_IGN:
		++*pos;
		return NULL;
	case UNFP_CONST:
	case UNFP_VAR:
		if (*pos + sizeof(Rid) + 1 > max) {
			*pos = max;
			return "Not enough space for id";
		}

		*pos += sizeof(Rid) + 1;
		return NULL;
	case UNFP_ORDER:
		if (*pos + sizeof(uint64_t) + 1 > max) {
			*pos = max;
			return "Not enough space for order";
		}

		*pos += sizeof(uint64_t) + 1;
		return NULL;
	case UNFP_LNIL:
	case UNFP_LOPEN:
		return unfp_list_deserable(buf, pos, max);
	default:
		break;
	}

	return "Not a valid term type";
}

/* list */

void
unfp_list_print(const Unfy_list *list)
{
	printf("(");

	if (!list) {
		printf(")");
		return;
	}

	unfp_term_print(list->term);
	list = list->next;

	for (; list; list = list->next) {
		printf(" ");
		unfp_term_print(list->term);
	}

	printf(")");
}

int
unfp_list_parse(Unfy_list **list, const char *str, size_t *pos,
		const char **error, Unfy_recycle *rec)
{
	int first = 1;
	Unfy_list **tail = list;

	*list = NULL;

	if (*str != '(') {
		*error = "No bracket at start of list";
		return -1;
	}

	++*pos;
	++str;

	for (; isspace(*str); ++*pos, ++str);

	while (*str != ')') {
		size_t old = *pos;
		Unfy_term *term;

		if (!first) {
			if (unfp_space(str, pos, error) < 0)
				return -1;

			if (*(str += *pos - old) == ')')
				break;
		} else {
			first = 0;
		}

		old = *pos;

		if (!(term = unfp_term_parse(str, pos, error, rec))) {
			unfy_recycle_list(rec, *list);
			return -1;
		}

		str += *pos - old;

		if (!(tail = unfy_list_append(tail, term, rec))) {
			*error = "Failed to append to list";
			unfy_recycle_term(rec, term);
			unfy_recycle_list(rec, *list);
			return -1;
		}
	}

	++*pos;
	return 0;
}

size_t
unfp_list_len(const Unfy_list *list)
{
	size_t len = 2;

	if (!list)
		return 1;

	for (; list; list = list->next)
		len += unfp_term_len(list->term);

	return len;
}

size_t
unfp_list_write(const Unfy_list *list, unsigned char *buf)
{
	int len = 1;

	if (!list) {
		*buf = UNFP_LNIL;
		return 1;
	}

	*buf = UNFP_LOPEN;

        for (; list; list = list->next)
		len += unfp_term_write(list->term, buf + len);

	buf[len] = UNFP_LCLOSE;
	return len + 1;
}

int
unfp_list_ser(const Unfy_list *list, unsigned char **buf,
	      size_t *size, size_t *pos)
{
	size_t len = unfp_list_len(list);

	if (*size < len) {
		unsigned char *new_buf = realloc(*buf, len);

		if (!new_buf)
			return -1;

	        *buf = new_buf;
		*size = len;
	}

	unfp_list_write(list, *buf);
	*pos += len;
	return 0;
}

int
unfp_list_deser(Unfy_list **list, const unsigned char *buf,
		size_t *pos, Unfy_recycle *rec)
{
	Unfy_list **end = list;

	*list = NULL;

	if (*buf == UNFP_LNIL) {
		++*pos;
		return 0;
	}

        if (*buf != UNFP_LOPEN)
		return -1;

	++*pos;
	++buf;

        do {
		Unfy_term *term;
		size_t tmp = 0;

		if (unfp_term_deser(&term, buf, &tmp, rec) < 0) {
			unfy_recycle_list(rec, *list);
			return -1;
		}

		if (!(end = unfy_list_append(end, term, rec))) {
			unfy_recycle_term(rec, term);
			unfy_recycle_list(rec, *list);
			return -1;
		}

		*pos += tmp;
		buf += tmp;
	} while (*buf != UNFP_LCLOSE);

	++*pos;

	return 0;
}

const char *
unfp_list_deserable(const unsigned char *buf, size_t *pos, const size_t max)
{
	const char *error;

	if (!buf || *pos >= max)
		return "Not enough space for list";

	switch (*buf) {
	case UNFP_LNIL:
		++*pos;
		return NULL;
	case UNFP_LOPEN:
		break;
	default:
	        return "Missing list byte";
	}

	++*pos;
	++buf;

	do {
		size_t tmp = *pos;

		if ((error = unfp_term_deserable(buf, pos, max)))
			return error;

		if (*pos + 1 > max) {
			*pos = max;
			return "Missing list close byte";
		}

		buf += *pos - tmp;
	} while (*buf != UNFP_LCLOSE);

	++*pos;
	return NULL;
}

/* bind */

void
unfp_bind_print(const Unfy_bind *bind)
{
	Rid_fn85 str;

	if (!bind) {
		printf("EMPTY");
		return;
	}

	rid_fn85(str, bind->var_id);
	printf("var %s -> ", str);
	unfp_term_print(bind->term);
}

void
unfp_binds_print(const Unfy_bind *bind, int level)
{
	if (!bind) {
		for (int i = 0; i < level; ++i)
			printf("\t");

		printf("EMPTY\n");
		return;
	}

	for (; bind; bind = bind->next) {
		for (int i = 0; i < level; ++i)
			printf("\t");

		unfp_bind_print(bind);
		printf("\n");
	}
}

int
unfp_space(const char *str, size_t *pos, const char **error)
{
	if (!isspace(*str)) {
		*error = "Missing a space";
		return -1;
	}

	++str;
	++*pos;

	for (; isspace(*str); ++str, ++*pos);
	return 0;
}

size_t
unfp_bind_len(const Unfy_bind *bind)
{
	size_t len = 2;

	if (!bind)
		return 1;

	for (; bind; bind = bind->next)
		len += unfp_term_len(bind->term) + sizeof(bind->var_id);

	return len;
}

size_t
unfp_bind_write(const Unfy_bind *bind, unsigned char *buf)
{
	int len = 1;

	if (!bind) {
		*buf = UNFP_BNIL;
		return 1;
	}

	*buf = UNFP_BOPEN;

        for (; bind; bind = bind->next) {
		len += unfp_term_write(bind->term, buf + len);
		rid_set(buf + len, bind->var_id);
		len += sizeof(bind->var_id);
	}

	buf[len] = UNFP_BCLOSE;
	return len + 1;
}

int
unfp_bind_ser(const Unfy_bind *bind, unsigned char **buf,
	      size_t *size, size_t *pos)
{
	size_t len = unfp_bind_len(bind);

	if (*size < len) {
		unsigned char *new_buf = realloc(*buf, len);

		if (!new_buf)
			return -1;

	        *buf = new_buf;
		*size = len;
	}

	unfp_bind_write(bind, *buf);
	*pos += len;
	return 0;
}

int
unfp_bind_deser(Unfy_bind **bind, const unsigned char *buf,
		size_t *pos, Unfy_recycle *rec)
{
	Unfy_bind **end = bind;

	*bind = NULL;

	if (*buf == UNFP_BNIL) {
		++*pos;
		return 0;
	}

	if (*buf != UNFP_BOPEN)
		return -1;

	++*pos;
	++buf;

        do {
		Unfy_term *term;
		size_t tmp = 0;
		Rid var_id;

		if (unfp_term_deser(&term, buf, &tmp, rec) < 0) {
			unfy_recycle_bind(rec, *bind);
			return -1;
		}

		rid_set(var_id, buf + tmp);
		tmp += sizeof(var_id);

		if (unfy_bind(end, var_id, term, rec) < 0) {
			unfy_recycle_term(rec, term);
			unfy_recycle_bind(rec, *bind);
			return -1;
		}

		*pos += tmp;
		buf += tmp;
		*(end = &(*end)->next) = NULL;
	} while (*buf != UNFP_BCLOSE);

	++*pos;

	return 0;
}

const char *
unfp_bind_deserable(const unsigned char *buf, size_t *pos, const size_t max)
{
	const char *error;

	if (!buf || *pos >= max)
		return "Not enough space for bind";

	switch (*buf) {
	case UNFP_BNIL:
		++*pos;
		return NULL;
	case UNFP_BOPEN:
		break;
	default:
	        return "Missing bind byte";
	}

	++*pos;
	++buf;

	do {
		size_t tmp = *pos;

		if ((error = unfp_term_deserable(buf, pos, max)))
			return error;

		if (*pos + sizeof(Rid) + 1 > max) {
			*pos = max;
			return "Content of bind exceeds maximum";
		}

		*pos += sizeof(Rid);
		buf += *pos - tmp;
	} while (*buf != UNFP_BCLOSE);

	++*pos;
	return NULL;
}
